package Project4;
import java.util.*;

public class SymbolTable{
	Hashtable<String, Variable> variables = new Hashtable<String, Variable>();
	Hashtable<String, Method> methods = new Hashtable<String, Method>();

	/* BLOCK SCOPE
		All the blocks are linked together in a chain. Allows for nested IF statements and such.
	*/
    private LinkedList<Method> blockScopes = new LinkedList<Method>();

    public void AddBlockScope(){
        blockScopes.add(new Method("block_scope_temp", "VOID"));
    }

    public void RemoveBlockScope(){
        blockScopes.removeLast();
    }

    public void PrintErrorMessage(String codeError){
        System.out.println("ERROR WITH THIS CODE: " + codeError);
    }

    public void PrintParameterLengthMismatchForMethod(String methodName, ArrayList<String>typeList){
        this.PrintErrorMessage(methodName + "( <VARS> );");
        System.out.println("\tYou have tried to invoke a method with " + typeList.size() + " arguments.");
        System.out.println("\t" + methodName + " requires " + this.getMethod(methodName).getParams().size() + " arguments.");
        System.out.println("\tEither add/remove some arguments or call a different method.");
    }


    public void PrintParameterTypeMismatchForMethod(String methodName, String declaredType, String givenType){
        this.PrintErrorMessage(methodName + "( <VARS>);");
        System.out.println("\tWrong variable type was provided for the given argument.");
        System.out.println("\tExpecting " + declaredType + " but got " + givenType + ".");
        System.out.println("\tEnsure that you are passing the correct variable to the method.");
    }

    public void PrintDuplicateVarDeclaration(Variable v){
        PrintErrorMessage(v.getName() + ":" + v.getType() + ";");
		String match = fuzzyLookup(v.getName(), v.getType()); 
        System.out.println("\tVariable has already been declared in this scope.");
        System.out.println("\tYou cannot redeclare the variable, as you would lose it.");
        System.out.println("\tIt could also be spelled wrong" + 
				((match.equals("")) ? "." : ", did you mean: \""+match+"\"?"));
    }

    public void PrintVariableNotDeclared(String variableName, String variableType, String codeError){
        PrintErrorMessage(codeError);
		String match = fuzzyLookup(variableName, variableType); 
        System.out.println("\tVariable has not been declared, perhaps you neglected to declare it?");
        System.out.println("\tVariable declaration should appear as: " + variableName + ":" + variableType);
        System.out.println("\tIt could also be spelled wrong" +
				((match.equals("")) ? "." : ", did you mean: \""+match+"\"?"));
    }

    public void PrintFunctionNotDeclared(String functionName, String code){
        PrintErrorMessage(code);
		String match = "";//fuzzyLookupTypeless(functionName);
        System.out.println("\tMethod has not been declared, perhaps you neglected to declare it?");
        System.out.println("\tIt could also be spelled wrong" +
				((match.equals("") ? "." : ", did you mean: \"")+match+"\"?"));
    }

    public void PrintVariableTypeMismatch(String variableName, String correctType, String wrongType, String codeError){
        PrintErrorMessage(codeError);
		String match = fuzzyLookup(variableName, wrongType); 
        System.out.println("\tType mismatch! Expecting " + correctType + " but got " + wrongType);
        System.out.println("\tVariable declaration should appear as: " + variableName + ":" + wrongType);
        System.out.println("\tEither declare " + variableName + " as " + wrongType + " or use a different variable" +
				((match.equals("")) ? "." : ", perhaps \""+match+":"+wrongType+"\"?"));
    }

	public void PrintReturnOutsideMethod(String t){
		PrintErrorMessage("RETURN <" + t + ">;");
		System.out.println("\tNot inside a method! Expecting to be in a method.");
		System.out.println("\tPlease only return values from inside methods.");
	}

	public void PrintReturnTypeMismatch(String t, Method m){
		PrintErrorMessage("RETURN <" + t + ">;");
		System.out.println("\tWrong return type for method " + m.getName());
		System.out.println("\tGot " + t + " but expecting " + m.getType());
		System.out.println("\tPerhaps you are using the wrong variable?");
	}

	public void CheckReturnType(String type, Method m){
		if(m==null){
			PrintReturnOutsideMethod(type);
		}
		else if(!type.equals(m.getType())){
			PrintReturnTypeMismatch(type, m);
		}
	}

    public void CheckDeclaredAndTypeForVar(String id, String type, String code, Method m){
        if(CheckDeclaredVar(id, m)) {
            String tempType = "";
            boolean useBlock = false;
            
            if(this.blockScopes.size() > 0){
                for(Method method: this.blockScopes){
                    if(method.containsVar(id)){
                        tempType = method.getVar(id).getType();
                        useBlock = true;
                        break;
                    }
                }
            }
            if(m != null && m.containsVar(id) && !useBlock){
                tempType = m.getVar(id).getType();
            }
            else if(!useBlock){                
                tempType = this.getVar(id).getType();
            }
            
            if(!tempType.equals(type)){
                PrintVariableTypeMismatch(id, tempType, type, code);
            }
        }
    }

	public void CheckIfAllDeclaredAndType(
			ArrayList<String> ids, ArrayList<String> types, Method m){
		for(int i = 0; i < ids.size(); i++){
			CheckDeclaredAndTypeForVar(ids.get(i), types.get(i),
				"<method containing> " + ids.get(i) + ":" + types.get(i), m);
		}
	}

    public boolean CheckDeclaredFunction(String id){
        if(!this.containsMethod(id)){
            PrintFunctionNotDeclared(id, id+"(<vars>)");
			return false;
        }

		return true;
    }

    public boolean CheckDeclaredVar(String id, Method m){
        boolean declared = false;
        for(Method method: this.blockScopes){
            declared = method.containsVar(id) || declared;
        }

        if(m != null){
            declared = m.containsVar(id) || declared;
        }

        declared = this.containsVar(id) || declared;
        
        if(!declared){
            PrintVariableNotDeclared(id, "INT", id);
        }

        return declared;
    }

    public boolean CheckDuplicateVarDeclaration(Method m, Variable v){
        boolean duplicate = false;

        if(this.blockScopes.size() != 0){
            //there is at least one scope
            for (Method method: this.blockScopes){
                duplicate = method.containsVar(v.getName()) || duplicate;
            }
        }

        if(m != null){
            duplicate = m.containsVar(v.getName()) || duplicate;
        }

        duplicate = this.containsVar(v.getName()) || duplicate;

        if(duplicate){
            this.PrintDuplicateVarDeclaration(v);
        }

        return duplicate;
    }

    public void typeCheck(String a, String b){
        if(!a.equals(b)){
			System.out.println("Expression has incompatible types: expected " + a + " but got " + b);
		}
    }

	public boolean compareParams(String methodName, ArrayList<String> typeList){
		ArrayList<Variable> declaredParams = this.getMethod(methodName).getParams();

		if(typeList.size() != declaredParams.size()){
			//param length mismatch
            this.PrintParameterLengthMismatchForMethod(methodName, typeList);
			return false;
		}

		for(int i = 0; i < typeList.size(); i++){
			if(!declaredParams.get(i).getType().equals(typeList.get(i))){
                this.PrintParameterTypeMismatchForMethod(methodName, declaredParams.get(i).getType(), typeList.get(i));
				return false;
			}
		}

		return true;
	}

	public boolean AddMethod(Method m){
        if(this.blockScopes.size() != 0){
			//get total var list
			ArrayList<Variable> params = m.getParams();
			String paramList = "";
			for(Variable v : params){
				paramList += v.getName()+":"+v.getType()+", ";
			}

			paramList = paramList.substring(0, paramList.length()-2);

			this.PrintErrorMessage(m.getType() + " " + m.getName() + "("+ paramList +"){");
			System.out.println("\tCannot decalre method in blocking scope (such as IF, WHILE, FOR, SWITCH, etc)");
        }
        else{
		    this.methods.put(m.getName(), m);
        }

        return true;
	}

	public boolean containsMethod(String id){
		return this.methods.containsKey(id);
	}

	public Method getMethod(String id){
		return this.methods.get(id);
	}

	public boolean addVar(Variable v, Method m){
        if(!CheckDuplicateVarDeclaration(m,v)){
            if(this.blockScopes.size() > 0){
                this.blockScopes.peekLast().addVar(v);
            }
            else if(m != null){
                m.addVar(v);
            }
            else{
                this.variables.put(v.getName(), v);
            }

            return true;
        }
        return false;
	}

	public boolean containsVar(String id){
		return this.variables.containsKey(id);
	}

	public Variable getVar(String id){
		return this.variables.get(id);
	}

	public Variable getVarFromAllScopes(String id, Method m){
		if(this.blockScopes.size() > 0){
			for(Method method : this.blockScopes){
				if(method.containsVar(id)){
					return method.getVar(id);
				}
			}
		}

		if(m != null && m.containsVar(id)){
			return m.getVar(id);
		}

		return this.getVar(id);
	}

	private String fuzzyLookup(String testString, String testType){
		HashSet<String> permutations = new HashSet<String>();
		String match = "";

		if(testString.length() < 2){
			permutations.add(testString);
			match = reallyFuzzyLookup(permutations, testString, testType);
			return match;
		}
		
		permuteString("", testString, permutations);

		match = fuzzyLookup(permutations, testString, testType);
		if(!match.equals(""))
			return match;

		for(int i = 0; i < testString.length(); i++)
			permuteString("",testString.replaceFirst(Character.toString(testString.charAt(i)),""),permutations);

		match = fuzzyLookup(permutations, testString, testType);
		if(!match.equals(""))
			return match;
		
		match = reallyFuzzyLookup(permutations, testString, testType);
		return match;
	}

	private String fuzzyLookupTypeless(String testString){
		HashSet<String> permutations = new HashSet<String>();
		String match = "";

		if(testString.length() < 2){
			permutations.add(testString);
			match = reallyFuzzyLookupTypeless(permutations, testString);
			return match;
		}
		
		permuteString("", testString, permutations);

		match = fuzzyLookupTypeless(permutations, testString);
		if(!match.equals(""))
			return match;

		for(int i = 0; i < testString.length(); i++)
			permuteString("",testString.replaceFirst(Character.toString(testString.charAt(i)),""),permutations);

		match = fuzzyLookupTypeless(permutations, testString);
		if(!match.equals(""))
			return match;
		
		match = reallyFuzzyLookupTypeless(permutations, testString);
		return match;
	}

	private void permuteString(String beginningString, String endingString, HashSet<String> strings){
		if (endingString.length() <= 1){
			strings.add((beginningString + endingString).toLowerCase());
		}
		else{
			for (int i = 0; i < endingString.length(); i++){
				String newString = endingString.substring(0, i) + endingString.substring(i + 1);
				permuteString(beginningString + endingString.charAt(i), newString, strings);
			}
		}
	}

	private String fuzzyLookup(HashSet<String> permutations, String var, String type){
		for(String permutation : permutations)
			for(String tableString : variables.keySet())
				if(variables.get(tableString).getType().toLowerCase().equals(type.toLowerCase())
							&& tableString.toLowerCase().equals(permutation)
							&& !tableString.toLowerCase().equals(var))
					return tableString;
		return "";
	}

	private String reallyFuzzyLookup(HashSet<String> permutations, String var, String type){
		for(String permutation : permutations)
			for(String tableString : variables.keySet())
				if(variables.get(tableString).getType().toLowerCase().equals(type.toLowerCase())
							&& tableString.toLowerCase().contains(permutation)
							&& !tableString.toLowerCase().equals(var))
					return tableString;
		return "";
	}
	
	private String fuzzyLookupTypeless(HashSet<String> permutations, String var){
		for(String permutation : permutations)
			for(String tableString : methods.keySet())
				if(tableString.toLowerCase().equals(permutation) && !tableString.toLowerCase().equals(var))
					return tableString;
		return "";
	}

	private String reallyFuzzyLookupTypeless(HashSet<String> permutations, String var){
		for(String permutation : permutations)
			for(String tableString : methods.keySet())
				if(tableString.toLowerCase().contains(permutation) && !tableString.toLowerCase().equals(var))
					return tableString;
		return "";
	}
}
